import type { Mark, Node, Schema } from "@rivet/model";

import { Transaction } from "./transaction.ts";
import type { Plugin, StateField } from "./plugin.ts";

function bind<T extends Function>(f: T, self: any): T {
  return !self || !f ? f : f.bind(self);
}

class FieldDesc<T> {
  name: string;
  init: (config: PatchStateConfig, instance: PatchState) => T;
  apply: (
    tr: Transaction,
    value: T,
    oldState: PatchState,
    newState: PatchState,
  ) => T;

  constructor(name: string, desc: StateField<any>, self?: any) {
    this.name = name;
    this.init = bind(desc.init, self);
    this.apply = bind(desc.apply, self);
  }
}

const baseFields = [
  new FieldDesc<Node>("doc", {
    init(config) {
      return config.doc;
    },
    apply(tr) {
      return tr.doc;
    },
  }),
];

class Configuration {
  fields: FieldDesc<any>[];
  plugins: Plugin[] = [];
  pluginsByKey: { [key: string]: Plugin } = Object.create(null);
  schema: Schema;

  constructor(schema: Schema, plugins?: Plugin[]) {
    this.fields = baseFields.slice();
    this.schema = schema;
    if (plugins) {
      plugins.forEach((plugin) => {
        if (this.pluginsByKey[plugin.key]) {
          throw new RangeError("请不要重复添加 (" + plugin.key + ")");
        }
        this.plugins.push(plugin);
        this.pluginsByKey[plugin.key] = plugin;
        if (plugin.spec.state) {
          this.fields.push(
            new FieldDesc<any>(plugin.key, plugin.spec.state, plugin),
          );
        }
      });
    }
  }
}

export interface PatchStateConfig {
  schema?: Schema;

  doc?: Node;

  storedMarks?: Mark[] | null;

  plugins?: Plugin[];
}

/*
状态管理 管理编辑器状态
*/
export class PatchState {
  config: Configuration;

  constructor(
    config: Configuration,
  ) {
    this.config = config;
  }

  doc!: Node;

  get schema(): Schema {
    return this.config.schema;
  }

  get plugins(): Plugin[] {
    return this.config.plugins;
  }

  apply(
    tr: Transaction,
  ): PatchState {
    return this.applyTransaction(tr).state;
  }

  /// @internal
  filterTransaction(tr: Transaction, ignore = -1): boolean {
    for (let i = 0; i < this.config.plugins.length; i++) {
      if (i != ignore) {
        const plugin = this.config.plugins[i];
        if (
          plugin.spec.filterTransaction &&
          !plugin.spec.filterTransaction.call(plugin, tr, this)
        ) {
          return false;
        }
      }
    }
    return true;
  }

  /**
   * 生成新的state 并返回
   * 初始检查：检查传入的事务是否通过过滤器，如果不通过则直接返回当前状态和空事务列表。
   * 初始化变量：初始化事务列表 trs 和新状态 newState，并创建一个 seen 数组来记录每个插件的状态和事务数量。
   * 主循环：遍历所有插件，调用插件的 appendTransaction 方法生成新的事务，如果生成了新的事务，则将其添加到事务列表中，并更新新状态。
   * 退出条件：如果没有新的事务生成，则退出循环。
   * 返回结果：返回最终的新状态和事务列表。
   * @param rootTr
   */
  applyTransaction(
    rootTr: Transaction,
  ): { state: PatchState; transactions: Transaction[] } {
    if (!this.filterTransaction(rootTr)) {
      return { state: this, transactions: [] };
    }
    //trs 事务集合  newState 初始事务对应的 新的state  seen 用于存储已经处理过的插件 产生的新的 state
    let trs = [rootTr], newState = this.applyInner(rootTr), seen = null;
    for (;;) {
      //判断是否会产生新的事务
      let haveNew = false;
      //遍历插件
      for (let i = 0; i < this.config.plugins.length; i++) {
        const plugin = this.config.plugins[i];
        if (plugin.spec.appendTransaction) {
          const n = seen ? seen[i].n : 0,
            oldState = seen ? seen[i].state : this;
          const tr = n < trs.length &&
            plugin.spec.appendTransaction.call(
              plugin,
              n ? trs.slice(n) : trs,
              oldState,
              newState,
            );
          //如果产生了新的事务 则添加到数组中 并进行下一次循环
          if (tr && newState.filterTransaction(tr, i)) {
            tr.setMeta("appendedTransaction", rootTr);
            if (!seen) {
              seen = [];
              //初始化插件状态数组 并初始化插件状态
              for (let j = 0; j < this.config.plugins.length; j++) {
                seen.push(
                  j < i
                    ? { state: newState, n: trs.length }
                    : { state: this, n: 0 },
                );
              }
            }
            trs.push(tr);
            //更新产生新的 state
            newState = newState.applyInner(tr);
            haveNew = true;
          }
          if (seen) seen[i] = { state: newState, n: trs.length };
        }
      }
      if (!haveNew) return { state: newState, transactions: trs };
    }
  }

  /// @internal
  /**
   * 应用事务 并产生新的state
   * @param tr
   */
  applyInner(tr: Transaction): PatchState {
    const newInstance = new PatchState(this.config),
      fields = this.config.fields;
    for (let i = 0; i < fields.length; i++) {
      const field = fields[i];
      (newInstance as any)[field.name] = field.apply(
        tr,
        (this as any)[field.name],
        this,
        newInstance,
      );
    }
    return newInstance;
  }

  get tr(): Transaction {
    return new Transaction(this);
  }

  /// Create a new state.
  static create(config: PatchStateConfig): PatchState {
    const $config = new Configuration(
      config.doc ? config.doc.type.schema : config.schema!,
      config.plugins,
    );
    const instance = new PatchState($config);
    for (let i = 0; i < $config.fields.length; i++) {
      (instance as any)[$config.fields[i].name] = $config.fields[i].init(
        config,
        instance,
      );
    }
    return instance;
  }

  reconfigure(config: {
    plugins?: Plugin[];
  }): PatchState {
    const $config = new Configuration(this.schema, config.plugins);
    const fields = $config.fields, instance = new PatchState($config);
    for (let i = 0; i < fields.length; i++) {
      const name = fields[i].name;
      (instance as any)[name] = this.hasOwnProperty(name)
        ? (this as any)[name]
        : fields[i].init(config, instance);
    }
    return instance;
  }

  toJSON(pluginFields?: { [propName: string]: Plugin }): any {
    let result: any = { doc: this.doc.toJSON() };
    if (pluginFields && typeof pluginFields == "object") {
      for (let prop in pluginFields) {
        if (prop == "doc") {
          throw new RangeError("json 里面不能使用 `doc` 这个变量名称");
        }
        let plugin = pluginFields[prop], state = plugin.spec.state;
        if (state && state.toJSON) {
          result[prop] = state.toJSON.call(plugin, (this as any)[plugin.key]);
        }
      }
    }
    return result;
  }
}
